﻿#define NOMINMAX
#include <cmath>
#include "atlstr.h"
#include "Windows.h"
#include <atomic>
#include <functional>
#include "DeviceModel.h"

using namespace proto;
static uint8_t frameBuffer[256]{};

class RmsReveiver : public util::audio::AudioRmsReceiver {
public:
	RmsReveiver(DeviceModel *model) : devModel(model) {
	}

	~RmsReveiver() {
	}

	void OnReceiveRMS(int tag, double time, double rms) override {
		if (devModel) {
			rms *= rmsFactor;
			if (rms > 100.0) {
				rms = 100.0;
			}
			devModel->LedSetFixedLightness(rms);
		}
	}

	void ChangeFactor(double factor) {
		//rmsFactor = factor;
	}

private:
	std::atomic<double> rmsFactor = 1.0;
	DeviceModel *devModel = nullptr;
};

DeviceModel::DeviceModel(DeviceDataReceiver* recvr, const std::wstring& devName, const std::wstring &serPath)
	: devDataRecvr(recvr)
	, deviceName(devName)
	, serialPath(serPath)
	, protoEncoder(frameEncodeBuffer, proto::FrameAddrDevice)
	, protoDecoder(frameDecodeBuffer, proto::FrameAddrHost)
{
}

DeviceModel::~DeviceModel() {
	if (IsConnected()) {
		Disconnect();
	}
}

bool DeviceModel::Connect(unsigned int baudRate) {
	if (!devDataRecvr) {
		return false;
	}
	if (IsConnected() || serialPath.empty()) {
		return false;
	}
	if (baudRate < 9600 || baudRate > 1000000) {
		return false;
	}

	std::string dev = CW2A(this->serialPath.c_str(), CP_ACP).m_psz;
	try {
		serialPort = std::make_unique<CallbackAsyncSerial>(dev, baudRate);
	}
	catch (...) {
		serialPort.reset();
		return false;
	}
	if (!serialPort->isOpen()) {
		serialPort.reset();
		return false;
	}
	serialPort->setCallback(
		std::bind(&DeviceModel::OnRecvDeviceData, this, std::placeholders::_1, std::placeholders::_2)
	);
	StopReport();
	return true;
}

bool DeviceModel::IsConnected() const {
	return !!serialPort;
}

bool DeviceModel::Disconnect() {
	StopReport();
	::Sleep(10);
	serialPort->close();
	serialPort.reset();
	return true;
}

bool DeviceModel::ResetReportTime() {
	// [tag][00]
	return SendOneCommand(
		DevCmd{ proto::FrameTag::ResetTimeBase, {} }
	);
}

bool DeviceModel::StartReport() {
	// [tag][01][start(1)]
	return SendOneCommand(
		DevCmd{ proto::FrameTag::LedStatusReportCfg, {1} }
	);
}

bool DeviceModel::StopReport() {
	// [tag][01][stop(0)]
	return SendOneCommand(
		DevCmd{ proto::FrameTag::LedStatusReportCfg, {0} }
	);
}

bool DeviceModel::LedSetFixedLightness(double lightness) {
	// [tag][01][fdc(0~255)]
	if (lightness < 0 || lightness > 100) {
		return false;
	}
	lightness *= 2.55;
	lightness = std::round(lightness);
	return SendOneCommand(DevCmd{ proto::FrameTag::PwmFixedDutyCycle, {static_cast<uint8_t>(lightness)}});
}

bool DeviceModel::ConfigLedWaveMode(proto::FrameTag tag, unsigned int period) {
	// [tag][04][period_low][period_high][pwm_min][pwm_high]
	if (period < 500 || period > 30000) {
		return false;
	}
	return SendOneCommand(
		DevCmd
		{
			tag,
			{
				static_cast<uint8_t>(period & 0xff),
				static_cast<uint8_t>((period >> 8) & 0xff)
			}
		}
	);
}
bool DeviceModel::LedSetAudioRmsMode(double factor) {
	if (!audioRecvr) {
		audioRecvr = std::make_unique<RmsReveiver>(this);
	}
	static_cast<RmsReveiver *>(audioRecvr.get())->ChangeFactor(factor);
	if (!util::audio::IsAudioRecording()) {
		util::audio::StartAudioRecording(audioRecvr.get());
		if (!audioRecvr) {
			devDataRecvr = nullptr;
		}
	}
	LedSetFixedLightness(0);
	return true;
}

void DeviceModel::LeaveAudioRmsMode() {
	util::audio::StopAudioRecording();
}

bool DeviceModel::SendCommands(const std::vector<DevCmd> &cmds) {
	if (!serialPort || cmds.empty()) {
		return false;
	}

	protoEncoder.ClearFrame();
	for (auto &cmd : cmds) {
		auto ok = protoEncoder.AppendTLV(
			proto::Tag2Num(cmd.tag),
			static_cast<uint8_t>(cmd.value.size()),
			cmd.value.empty() ? nullptr : &cmd.value[0]
		);
		if (!ok) {
			protoEncoder.ClearFrame();
			return false;
		}
	}
	protoEncoder.AppendCRC();

	size_t frameLen = 0;
	const auto* frameData = protoEncoder.GetFrame(frameLen);
	if (!frameData || frameLen < 7) {
		return false;
	}
	try {
		serialPort->write(reinterpret_cast<const char *>(frameData), frameLen);
		return true;
	}
	catch (...) {
		return false;
	}
}

bool DeviceModel::SendOneCommand(const DevCmd &cmd) {
	if (!serialPort || cmd.value.size() > 248) {
		return false;
	}
	protoEncoder.ClearFrame();
	protoEncoder.AppendTLV(
		proto::Tag2Num(cmd.tag),
		static_cast<uint8_t>(cmd.value.size()),
		cmd.value.empty() ? nullptr : &cmd.value[0]
	);
	protoEncoder.AppendCRC();

	size_t frameLen = 0;
	const auto* frameData = protoEncoder.GetFrame(frameLen);
	if (!frameData || frameLen < 7) {
		return false;
	}
	try {
		serialPort->write(reinterpret_cast<const char *>(frameData), frameLen);
		return true;
	}
	catch (...) {
		return false;
	}
}

// run in boost.asio worker thread
void DeviceModel::OnRecvDeviceData(const char *dataBytes, std::size_t byteCnt) {
	if (!dataBytes || !byteCnt) {
		return;
	}

	auto cb = [this] (uint8_t tag, uint8_t len, const uint8_t *val) {
		if (!devDataRecvr) {
			return false;
		}
		std::string buffer;
		if (len > 0) {
			buffer.assign(reinterpret_cast<const char*>(val), len);
		}
		switch (proto::FrameTag{ tag })
		{
		case proto::FrameTag::ReportLedStatus:
			return OnRecvLedStatus(std::move(buffer));
		case proto::FrameTag::DataAck:
			return OnRecvDataACK(std::move(buffer));
		default:
			break;
		}
		return true;
	};

	for (size_t i = 0; i < byteCnt; i++) {
		auto status = protoDecoder.AppendByte(dataBytes[i]);
		if (status == proto::FrameDecoder::Status::DecodeFailed) {
			protoDecoder.Reset();
			i--;
			continue;
		}
		else if (status == proto::FrameDecoder::Status::FrameEnd) {
			protoDecoder.GetTLVs(cb);
		}
	}
}

// [tag][03][light(0~255)][t0][t1]
// [tag][04][light(0~255)][t0][t1][t2]
// [tag][05][light(0~255)][t0][t1][t2][t3]
bool DeviceModel::OnRecvLedStatus(const std::string& data) {
	if (!devDataRecvr) {
		return false;
	}
	if (data.size() < 3 || data.size() > 5) {
		return false;
	}
	uint32_t msTick = 0;
	for (std::size_t i = data.size() - 1; i > 0; i--) {
		msTick <<= 8;
		msTick += static_cast<uint8_t>(data[i]);
	}
	devDataRecvr->OnReportLedStatus(
		// sec
		static_cast<double>(msTick) / 1000,
		// 0~100%
		static_cast<double>(static_cast<uint8_t>(data[0])) / 2.55
	);
	return true;
}

// [tag][2][data_crc0][data_crc1]
bool DeviceModel::OnRecvDataACK(const std::string& ack) {
	if (ack.size() != 2) {
		return false;
	}
	uint16_t crc = static_cast<uint8_t>(ack[1]);
	crc <<= 8;
	crc += static_cast<uint8_t>(ack[0]);
	//OutputDebugStringA("ACK from device\n");
	return true;
}